import org.hyperledger.fabric.sdk.*;
import org.hyperledger.fabric.sdk.exception.CryptoException;
import org.hyperledger.fabric.sdk.exception.InvalidArgumentException;
import org.hyperledger.fabric.sdk.exception.ProposalException;
import org.hyperledger.fabric.sdk.exception.TransactionException;
import org.hyperledger.fabric.sdk.security.CryptoSuite;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Paths;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.spec.InvalidKeySpecException;
import java.util.*;

import static org.apache.commons.compress.utils.CharsetNames.UTF_8;

/**
 * Created by xuguangyuansh on 2018/8/24.
 */
public class ChaincodeManager {

    private static final Logger logger = LoggerFactory.getLogger(ChaincodeManager.class);

    private FabricConfig config;
    private Orderers orderers;
    private Peers peers;
    private Chaincode chaincode;

    private HFClient client;
    private FabricOrg fabricOrg;
    private Channel channel;
    private ChaincodeID chaincodeID;

    public ChaincodeManager(String username, FabricConfig fabricConfig) throws InvalidKeySpecException, NoSuchAlgorithmException,
            NoSuchProviderException, IOException, TransactionException, InvalidArgumentException, IllegalAccessException,
            InvocationTargetException, InstantiationException, NoSuchMethodException, CryptoException, ClassNotFoundException {
        this.config = fabricConfig;

        orderers = this.config.getOrderers();
        peers = this.config.getPeers();
        chaincode = this.config.getChaincode();

        client = HFClient.createNewInstance();
        logger.debug("Create instance of HFClient");
        client.setCryptoSuite(CryptoSuite.Factory.getCryptoSuite());
        logger.debug("Set Crypto Suite of HFClient");

        fabricOrg = getFabricOrg(username);
        channel = getChannel();
        chaincodeID = getChaincodeID();

        client.setUserContext(fabricOrg.getPeerAdmin());    // 也许是 1.0.0 测试版的 Bug，只有节点管理员可以调用链码
    }

    private FabricOrg getFabricOrg(String username) throws NoSuchAlgorithmException, NoSuchProviderException,
            InvalidKeySpecException, IOException {
        // java.io.tmpdir : C:\Users\xuguangyuansh\AppData\Local\Temp\
        File storeFile = new File(System.getProperty("java.io.tmpdir") + "HFCSampletest.properties");
        FabricStore fabricStore = new FabricStore(storeFile);

        // Get Org1 from configuration
        FabricOrg fabricOrg = new FabricOrg(username, peers, orderers, fabricStore, config.getCryptoConfigPath());
        logger.debug("Get FabricOrg");
        return fabricOrg;
    }

    private Channel getChannel() throws InvalidArgumentException, TransactionException {
        client.setUserContext(fabricOrg.getPeerAdmin());
        return getChannel(fabricOrg, client);
    }

    private Channel getChannel(FabricOrg fabricOrg, HFClient client) throws InvalidArgumentException, TransactionException {
        Channel channel = client.newChannel(chaincode.getChannelName());
        logger.debug("Get Chain " + chaincode.getChannelName());

        // channel.setTransactionWaitTime(chaincode.getTransactionWaitTime());   // removed after 1.1.0
        // channel.setDeployWaitTime(chaincode.getDeployWaitTime());   // removed after 1.1.0

        for (Peers.Peer peer : peers.get()) {
            File peerCert = Paths.get(config.getCryptoConfigPath(), "/peerOrganizations", peers.getOrgDomainName(),
                    "peers", peer.getPeerName(), "tls/server.crt").toFile();
            if (!peerCert.exists()) {
                throw new RuntimeException(String.format("Missing cert file for: %s. Could not find at location: %s",
                        peer.getPeerName(), peerCert.getAbsolutePath()));
            }
            Properties peerProperties = new Properties();
            peerProperties.setProperty("pemFile", peerCert.getAbsolutePath());
            // ret.setProperty("trustServerCertificate", "true");   // testing
            // environment only NOT FOR PRODUCTION!
            peerProperties.setProperty("hostnameOverride", peers.getOrgDomainName());
            peerProperties.setProperty("sslProvider", "openSSL");
            peerProperties.setProperty("negotiationType", "TLS");
            // 在 grpc 的 NettyChannelBuilder 上设置特定选项
            peerProperties.put("grpc.ManagedChannelBuilderOption.maxInboundMessageSize", 9 * ((int) Math.pow(10, 6)));
            channel.addPeer(client.newPeer(peer.getPeerName(), fabricOrg.getPeerLocation(peer.getPeerName()), peerProperties));
            if (peer.isAddEventHub()) {
                channel.addEventHub((client.newEventHub(peer.getPeerEventHubName(),
                        fabricOrg.getEventHubLocation(peer.getPeerEventHubName()), peerProperties)));
            }
        }

        for (Orderers.Orderer orderer : orderers.get()) {
            File ordererCert = Paths.get(config.getCryptoConfigPath(), "/ordererOrganizations",
                    orderers.getOrdererDomainName(), "orderers", orderer.getOrdererName(), "tls/server.crt").toFile();
            if (!ordererCert.exists()) {
                throw new RuntimeException(String.format("Missing cert file for: %s. Could not find at location: %s",
                        orderer.getOrdererName(), ordererCert.getAbsolutePath()));
            }
            Properties ordererProperties = new Properties();
            ordererProperties.setProperty("pemFile", ordererCert.getAbsolutePath());
            ordererProperties.setProperty("hostnameOverride", orderers.getOrdererDomainName());
            ordererProperties.setProperty("sslProvider", "openSSL");
            ordererProperties.setProperty("negotiationType", "TLS");
            ordererProperties.put("grpc.ManagedChannelBuilderOption.maxInboundMesasgeSize", 9 * ((int) Math.pow(10, 6)));
            ordererProperties.setProperty("oprdererWaitTimeMilliSecs", "300000");
            channel.addOrderer(client.newOrderer(orderer.getOrdererName(),
                    fabricOrg.getOrdererLocation(orderer.getOrdererName()), ordererProperties));
        }

        logger.debug("channel.isInitialized() = " + channel.isInitialized());
        if (!channel.isInitialized()) channel.initialize();
        return channel;
    }

    private ChaincodeID getChaincodeID() {
        return ChaincodeID.newBuilder().setName(chaincode.getChaincodeName()).setVersion(
                chaincode.getChaincodeVersion()).setPath(chaincode.getChaincodePath()).build();
    }

    /**
     * 执行智能合约
     *
     * @param fcn  方法名
     * @param args 参数数组
     * @returnØ
     * @throws InvalidArgumentException
     * @throws ProposalException
     * @throws IOException
     */
    public Map<String, String> invoke(String fcn, String[] args) throws InvalidArgumentException, ProposalException, IOException {
        Map<String, String> resultMap = new HashMap<>();

        Collection<ProposalResponse> successful = new LinkedList<>();
        Collection<ProposalResponse> failed = new LinkedList<>();

        /// Send transaction proposal to all peers
        TransactionProposalRequest transactionProposalRequest = client.newTransactionProposalRequest();
        transactionProposalRequest.setChaincodeID(chaincodeID);
        transactionProposalRequest.setFcn(fcn);
        transactionProposalRequest.setArgs(args);

        Map<String, byte[]> tm2 = new HashMap<>();
        tm2.put("HyperLedgerFabric", "TransactionProposalRequest:JavaSDK".getBytes(UTF_8));
        tm2.put("method", "TransactionProposalRequest".getBytes(UTF_8));
        tm2.put("result", ":)".getBytes(UTF_8));
        transactionProposalRequest.setTransientMap(tm2);

        long currentStart = System.currentTimeMillis();
        Collection<ProposalResponse> transactionPropResp = channel.sendTransactionProposal(transactionProposalRequest, channel.getPeers());
        for (ProposalResponse response : transactionPropResp) {
            if (response.getStatus() == ProposalResponse.Status.SUCCESS) {
                successful.add(response);
            } else {
                failed.add(response);
            }
        }
        logger.info("channel send transaction proposal time = " + (System.currentTimeMillis() - currentStart));

        Collection<Set<ProposalResponse>> proposalConsistencySets = SDKUtils.getProposalConsistencySets(transactionPropResp);
        if (proposalConsistencySets.size() > 1) {
            logger.error("Expected only one set of consistent proposal responses but got " + proposalConsistencySets.size());
        }

        if (failed.size() > 0) {
            ProposalResponse firstTransactionProposalResponse = failed.iterator().next();
            logger.error("Not enough endorsers for inspeect: " + failed.size() + "endorser error: " + firstTransactionProposalResponse.getMessage() + ". Was verified: " + firstTransactionProposalResponse.isVerified());
            resultMap.put("code", "error");
            resultMap.put("data", firstTransactionProposalResponse.getMessage());
            return resultMap;
        } else {
            logger.info("Successfully received transaction proposal responses.");
            ProposalResponse resp = transactionPropResp.iterator().next();
            logger.debug("TransactionID: " + resp.getTransactionID());
            byte[] x = resp.getChaincodeActionResponsePayload();
            String resultAsString = null;
            if (x != null) {
                resultAsString = new String(x, "UTF-8");
            }
            logger.info("resultAsString = " + resultAsString);
            channel.sendTransaction(successful);
            resultMap.put("code", "success");
            resultMap.put("data", resultAsString);
            resultMap.put("txid", resp.getTransactionID());
            return resultMap;
        }
    }

    /**
     * 查询智能合约
     *
     * @param fcn  方法名
     * @param args 参数数组
     * @return
     * @throws InvalidArgumentException
     * @throws ProposalException
     * @throws IOException
     */
    public Map<String, String> query(String fcn, String[] args) throws InvalidArgumentException, ProposalException, IOException {
        QueryByChaincodeRequest queryByChaincodeRequest = client.newQueryProposalRequest();
        queryByChaincodeRequest.setFcn(fcn);
        queryByChaincodeRequest.setArgs(args);
        queryByChaincodeRequest.setChaincodeID(chaincodeID);

        logger.debug("## fcn: {}, args: {}", fcn, args);

        Map<String, byte[]> tm2 = new HashMap<>();
        tm2.put("HyperLedgerFabric", "QueryByChaincodeRequest:JavaSDK".getBytes(UTF_8));
        tm2.put("method", "QueryByChaincodeRequest".getBytes(UTF_8));
        queryByChaincodeRequest.setTransientMap(tm2);

        Map<String, String> resultMap = new HashMap<>();
        String payload = "";

        Collection<ProposalResponse> queryProposals = channel.queryByChaincode(queryByChaincodeRequest, channel.getPeers());
        for (ProposalResponse proposalResponse : queryProposals) {
            if (!proposalResponse.isVerified() || proposalResponse.getStatus() != ProposalResponse.Status.SUCCESS) {
                logger.debug("Failed query proposal from peer " + proposalResponse.getPeer().getName() + " status: " + proposalResponse.getStatus() + ". Messages: " + proposalResponse.getMessage() + ". Was verified: " + proposalResponse.isVerified());
                resultMap.put("code", "error");
                resultMap.put("data", "Failed query proposal from peer " + proposalResponse.getPeer().getName() + " status: " + proposalResponse.getStatus() + ". Messages: " + proposalResponse.getMessage() + ". Was verified: " + proposalResponse.isVerified());
            } else {
                payload = proposalResponse.getProposalResponse().getResponse().getPayload().toStringUtf8();
                logger.debug("Query payload from peer: " + proposalResponse.getPeer().getName());
                logger.debug("TransactionID: " + proposalResponse.getTransactionID());
                logger.debug("" + payload);
                resultMap.put("code", "success");
                resultMap.put("data", payload);
                resultMap.put("txid", proposalResponse.getTransactionID());
            }
        }
        return resultMap;
    }
}
